用 Go 编写一个 Composition 函数

Composition 函数(简称函数)是模板化 Crossplane 资源的自定义程序。 当你创建复合资源 (XR) 时,Crossplane 会调用 Composition 函数来决定它应该创建哪些资源。阅读 concepts 页面了解更多有关 Composition 函数的信息。

您可以使用通用编程语言为模板资源编写函数。 使用通用编程语言可以让函数对模板资源使用高级逻辑,如循环和条件。 本指南介绍如何使用 Go编写 Composition 函数。

Important
在阅读本指南之前,最好先熟悉一下 Composition 功能的工作原理

了解步骤

本指南介绍为XBucketsComposition 资源 (XR) 的composition函数。

 1apiVersion: example.crossplane.io/v1
 2kind: XBuckets
 3metadata:
 4  name: example-buckets
 5spec:
 6  region: us-east-2
 7  names:
 8  - crossplane-functions-example-a
 9  - crossplane-functions-example-b
10  - crossplane-functions-example-c

一个 XBuckets XR 有一个区域和一个桶名数组。 该函数将为名称数组中的每个条目创建一个亚马逊网络服务(AWS)S3 桶。

用 Go 编写函数

1.安装编写函数所需的工具 2.从模板初始化函数 3.编辑模板,添加函数逻辑 4.测试端到端函数 5.构建函数并将其推送到软件包注册库

本指南将详细介绍每个步骤。

安装编写函数所需的工具

用 Go 编写函数需要

Note
你不需要访问 Kubernetes 集群或 crossplane 控制平面,就能构建或测试 Composition 功能。

从模板初始化函数

使用 “crossplane beta xpkg init “命令初始化一个新函数。运行该命令时,它会以一个 GitHub 仓库为模板初始化你的函数。

1crossplane beta xpkg init function-xbuckets function-template-go -d function-xbuckets 
2Initialized package "function-xbuckets" in directory "/home/negz/control/negz/function-xbuckets" from https://github.com/crossplane/function-template-go/tree/91a1a5eed21964ff98966d72cc6db6f089ad63f4 (main)

使用 crossplane beta init xpkg 命令会创建一个名为 function-xbuckets 的目录。 运行该命令后,新目录应如下所示:

1ls function-xbuckets
2Dockerfile fn.go fn_test.go go.mod go.sum input/  LICENSE main.go package/  README.md renovate.json

fn.go “文件是添加函数代码的地方,了解模板中的其他一些文件非常有用:

  • main.go 运行函数。你不需要编辑 main.go
  • Dockerfile 运行函数。不需要编辑 Dockerfile
  • input 目录定义了函数的输入类型。
  • package 目录包含被引用用于构建函数包的元数据。
Tip

在 Crossplane CLI v1.14 中,“crossplane beta xpkg init “只是克隆了一个模板 GitHub 仓库。 未来的 CLI 发布将自动执行用新函数名称替换模板名称等任务。 详情请参见 Crossplane 问题 #4941

在开始添加代码之前,您必须进行一些更改:

  • 编辑 package/crossplane.yaml 更改 packages 的名称。
  • 编辑 go.mod 更改 Go 模块的名称。

将软件包命名为 function-xbuckets.

模块的名称取决于你想把函数代码保存在哪里。 如果你把 Go 代码推送到 GitHub,可以使用你的 GitHub 用户名。 例如 module github.com/negz/function-xbuckets

本指南中的函数不引用输入类型。 对于该函数,应删除 inputpackage/input 目录。

input 目录定义了一个 Go 结构,函数可以使用该结构从 Composition 中获取输入。Composition functions 文档解释了如何将输入传递给 Composition 函数。

package/input “目录包含由 “input “目录中的结构生成的 OpenAPI 模式。

Tip

如果您正在编写一个被引用的函数,请对输入进行编辑,以满足您的函数要求。

更改输入的种类和 API group。 不要使用 “Input “和 “template.fn.crossplane.io”,而应使用对函数有意义的名称。

当你编辑 input 目录下的文件时,你必须通过运行 go generate 来更新一些已生成的文件。 详见 input/generate.go

1go generate ./...

编辑模板,添加函数逻辑

您可以在运行函数方法中添加您的函数逻辑。 首次打开文件时,文件中包含一个 “hello world “函数。

 1func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) {
 2    f.log.Info("Running Function", "tag", req.GetMeta().GetTag())
 3
 4    rsp := response.To(req, response.DefaultTTL)
 5
 6    in := &v1beta1.Input{}
 7    if err := request.GetInput(req, in); err != nil {
 8    	response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
 9    	return rsp, nil
10    }
11
12    response.Normalf(rsp, "I was run with input %q", in.Example)
13    return rsp, nil
14}

所有 Go Composition 函数都有一个 “RunFunction “方法。 crossplane 会在一个RunFunctionRequest结构。

该函数通过返回一个RunFunctionResponse结构。

Tip
crossplane 使用 Protocol Buffers 生成 RunFunctionRequestRunFunctionResponse 结构。您可以在 Buf Schema Registry 中找到 RunFunctionRequestRunFunctionResponse 的详细模式。

编辑 RunFunction 方法,将其替换为以下代码。

 1func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) {
 2    rsp := response.To(req, response.DefaultTTL)
 3
 4    xr, err := request.GetObservedCompositeResource(req)
 5    if err != nil {
 6    	response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))
 7    	return rsp, nil
 8    }
 9
10    region, err := xr.Resource.GetString("spec.region")
11    if err != nil {
12    	response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind()))
13    	return rsp, nil
14    }
15
16    names, err := xr.Resource.GetStringArray("spec.names")
17    if err != nil {
18    	response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind()))
19    	return rsp, nil
20    }
21
22    desired, err := request.GetDesiredComposedResources(req)
23    if err != nil {
24    	response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req))
25    	return rsp, nil
26    }
27
28    _ = v1beta1.AddToScheme(composed.Scheme)
29
30    for _, name := range names {
31    	b := &v1beta1.Bucket{
32    		ObjectMeta: metav1.ObjectMeta{
33    			Annotations: map[string]string{
34    				"crossplane.io/external-name": name,
35    			},
36    		},
37    		Spec: v1beta1.BucketSpec{
38    			ForProvider: v1beta1.BucketParameters{
39    				Region: ptr.To[string](region),
40    			},
41    		},
42    	}
43
44    	cd, err := composed.From(b)
45    	if err != nil {
46    		response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{}))
47    		return rsp, nil
48    	}
49
50    	desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd}
51    }
52
53    if err := response.SetDesiredComposedResources(rsp, desired); err != nil {
54    	response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp))
55    	return rsp, nil
56    }
57
58    return rsp, nil
59}

展开下面的代码块以查看完整的 fn.go,包括导入和解释函数逻辑的注释。

  1package main
  2
  3import (
  4    "context"
  5
  6    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  7    "k8s.io/utils/ptr"
  8
  9    "github.com/upbound/provider-aws/apis/s3/v1beta1"
 10
 11    "github.com/crossplane/function-sdk-go/errors"
 12    "github.com/crossplane/function-sdk-go/logging"
 13    fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
 14    "github.com/crossplane/function-sdk-go/request"
 15    "github.com/crossplane/function-sdk-go/resource"
 16    "github.com/crossplane/function-sdk-go/resource/composed"
 17    "github.com/crossplane/function-sdk-go/response"
 18)
 19
 20// Function returns whatever response you ask it to.
 21type Function struct {
 22    fnv1beta1.UnimplementedFunctionRunnerServiceServer
 23
 24    log logging.Logger
 25}
 26
 27// RunFunction observes an XBuckets composite resource (XR). It adds an S3
 28// bucket to the desired state for every entry in the XR's spec.names array.
 29func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) {
 30    f.log.Info("Running Function", "tag", req.GetMeta().GetTag())
 31
 32    // Create a response to the request. This copies the desired state and
 33    // pipeline context from the request to the response.
 34    rsp := response.To(req, response.DefaultTTL)
 35
 36    // Read the observed XR from the request. Most functions use the observed XR
 37    // to add desired managed resources.
 38    xr, err := request.GetObservedCompositeResource(req)
 39    if err != nil {
 40    	// If the function can't read the XR, the request is malformed. This
 41    	// should never happen. The function returns a fatal result. This tells
 42    	// Crossplane to stop running functions and return an error.
 43    	response.Fatal(rsp, errors.Wrapf(err, "cannot get observed composite resource from %T", req))
 44    	return rsp, nil
 45    }
 46
 47    // Create an updated logger with useful information about the XR.
 48    log := f.log.WithValues(
 49    	"xr-version", xr.Resource.GetAPIVersion(),
 50    	"xr-kind", xr.Resource.GetKind(),
 51    	"xr-name", xr.Resource.GetName(),
 52    )
 53
 54    // Get the region from the XR. The XR has getter methods like GetString,
 55    // GetBool, etc. You can use them to get values by their field path.
 56    region, err := xr.Resource.GetString("spec.region")
 57    if err != nil {
 58    	response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.region field of %s", xr.Resource.GetKind()))
 59    	return rsp, nil
 60    }
 61
 62    // Get the array of bucket names from the XR.
 63    names, err := xr.Resource.GetStringArray("spec.names")
 64    if err != nil {
 65    	response.Fatal(rsp, errors.Wrapf(err, "cannot read spec.names field of %s", xr.Resource.GetKind()))
 66    	return rsp, nil
 67    }
 68
 69    // Get all desired composed resources from the request. The function will
 70    // update this map of resources, then save it. This get, update, set pattern
 71    // ensures the function keeps any resources added by other functions.
 72    desired, err := request.GetDesiredComposedResources(req)
 73    if err != nil {
 74    	response.Fatal(rsp, errors.Wrapf(err, "cannot get desired resources from %T", req))
 75    	return rsp, nil
 76    }
 77
 78    // Add v1beta1 types (including Bucket) to the composed resource scheme.
 79    // composed.From uses this to automatically set apiVersion and kind.
 80    _ = v1beta1.AddToScheme(composed.Scheme)
 81
 82    // Add a desired S3 bucket for each name.
 83    for _, name := range names {
 84    	// One advantage of writing a function in Go is strong typing. The
 85    	// function can import and use managed resource types from the provider.
 86    	b := &v1beta1.Bucket{
 87    		ObjectMeta: metav1.ObjectMeta{
 88    			// Set the external name annotation to the desired bucket name.
 89    			// This controls what the bucket will be named in AWS.
 90    			Annotations: map[string]string{
 91    				"crossplane.io/external-name": name,
 92    			},
 93    		},
 94    		Spec: v1beta1.BucketSpec{
 95    			ForProvider: v1beta1.BucketParameters{
 96    				// Set the bucket's region to the value read from the XR.
 97    				Region: ptr.To[string](region),
 98    			},
 99    		},
100    	}
101
102    	// Convert the bucket to the unstructured resource data format the SDK
103    	// uses to store desired composed resources.
104    	cd, err := composed.From(b)
105    	if err != nil {
106    		response.Fatal(rsp, errors.Wrapf(err, "cannot convert %T to %T", b, &composed.Unstructured{}))
107    		return rsp, nil
108    	}
109
110    	// Add the bucket to the map of desired composed resources. It's
111    	// important that the function adds the same bucket every time it's
112    	// called. It's also important that the bucket is added with the same
113    	// resource.Name every time it's called. The function prefixes the name
114    	// with "xbuckets-" to avoid collisions with any other composed
115    	// resources that might be in the desired resources map.
116    	desired[resource.Name("xbuckets-"+name)] = &resource.DesiredComposed{Resource: cd}
117    }
118
119    // Finally, save the updated desired composed resources to the response.
120    if err := response.SetDesiredComposedResources(rsp, desired); err != nil {
121    	response.Fatal(rsp, errors.Wrapf(err, "cannot set desired composed resources in %T", rsp))
122    	return rsp, nil
123    }
124
125    // Log what the function did. This will only appear in the function's pod
126    // logs. A function can use response.Normal and response.Warning to emit
127    // Kubernetes events associated with the XR it's operating on.
128    log.Info("Added desired buckets", "region", region, "count", len(names))
129
130    return rsp, nil
131}

此代码

1.从 RunFunctionRequest 获取观察到的 Composition 资源。 2.从观察到的 Composition 资源中获取区域和水桶名称。 3.为每个桶名添加一个所需的 S3 桶。 4.在 “RunFunctionResponse “中返回所需的 S3 存储桶。

代码使用了 Upbound’s AWS S3 Provider 中的 v1beta1.Bucket 类型。用 Go 编写函数的一个好处是,你可以使用 crossplane 在其 Provider 中使用的强类型结构体来composition资源。

您必须获取 AWS Provider Go 模块才能使用这种类型:

1go get github.com/upbound/[email protected]

crossplane 提供了一个软件开发工具包 (SDK),用于在Go 中编写composition函数。本函数被引用了 SDK 中的实用工具。特别是 requestresponse 包,使得使用 RunFunctionRequestRunFunctionResponse 类型变得更加容易。

Tip
请阅读 SDK 的 Go package documentation

测试端到端功能

通过添加单元测试和被引用 “crossplane beta render “命令来测试你的函数。

Go 对单元测试有丰富的支持。 当你从模板初始化一个函数时,它会在 fn_test.go中添加一些单元测试。这些测试遵循 Go 的 建议。它们只引用 Go 标准库中的 pkg/testinggoogle/go-cmp

要添加测试用例,请更新 TestRunFunction 中的 cases 映射。 展开下面的代码块,查看函数的完整 fn_test.go 文件。

  1package main
  2
  3import (
  4    "context"
  5    "testing"
  6    "time"
  7
  8    "github.com/google/go-cmp/cmp"
  9    "github.com/google/go-cmp/cmp/cmpopts"
 10    "google.golang.org/protobuf/testing/protocmp"
 11    "google.golang.org/protobuf/types/known/durationpb"
 12
 13    "github.com/crossplane/crossplane-runtime/pkg/logging"
 14
 15    fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
 16    "github.com/crossplane/function-sdk-go/resource"
 17)
 18
 19func TestRunFunction(t *testing.T) {
 20    type args struct {
 21    	ctx context.Context
 22    	req *fnv1beta1.RunFunctionRequest
 23    }
 24    type want struct {
 25    	rsp *fnv1beta1.RunFunctionResponse
 26    	err error
 27    }
 28
 29    cases := map[string]struct {
 30    	reason string
 31    	args args
 32    	want want
 33    }{
 34    	"AddTwoBuckets": {
 35    		reason: "The Function should add two buckets to the desired composed resources",
 36    		args: args{
 37    			req: &fnv1beta1.RunFunctionRequest{
 38    				Observed: &fnv1beta1.State{
 39    					Composite: &fnv1beta1.Resource{
 40    						// MustStructJSON is a handy way to provide mock
 41    						// resources.
 42    						Resource: resource.MustStructJSON(`{
 43    							"apiVersion": "example.crossplane.io/v1alpha1",
 44    							"kind": "XBuckets",
 45    							"metadata": {
 46    								"name": "test"
 47    							},
 48    							"spec": {
 49    								"region": "us-east-2",
 50    								"names": [
 51    									"test-bucket-a",
 52    									"test-bucket-b"
 53    								]
 54    							}
 55    						}`),
 56    					},
 57    				},
 58    			},
 59    		},
 60    		want: want{
 61    			rsp: &fnv1beta1.RunFunctionResponse{
 62    				Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(60 * time.Second)},
 63    				Desired: &fnv1beta1.State{
 64    					Resources: map[string]*fnv1beta1.Resource{
 65    						"xbuckets-test-bucket-a": {Resource: resource.MustStructJSON(`{
 66    							"apiVersion": "s3.aws.upbound.io/v1beta1",
 67    							"kind": "Bucket",
 68    							"metadata": {
 69    								"annotations": {
 70    									"crossplane.io/external-name": "test-bucket-a"
 71    								}
 72    							},
 73    							"spec": {
 74    								"forProvider": {
 75    									"region": "us-east-2"
 76    								}
 77    							}
 78    						}`)},
 79    						"xbuckets-test-bucket-b": {Resource: resource.MustStructJSON(`{
 80    							"apiVersion": "s3.aws.upbound.io/v1beta1",
 81    							"kind": "Bucket",
 82    							"metadata": {
 83    								"annotations": {
 84    									"crossplane.io/external-name": "test-bucket-b"
 85    								}
 86    							},
 87    							"spec": {
 88    								"forProvider": {
 89    									"region": "us-east-2"
 90    								}
 91    							}
 92    						}`)},
 93    					},
 94    				},
 95    			},
 96    		},
 97    	},
 98    }
 99
100    for name, tc := range cases {
101    	t.Run(name, func(t *testing.T) {
102    		f := &Function{log: logging.NewNopLogger()}
103    		rsp, err := f.RunFunction(tc.args.ctx, tc.args.req)
104
105    		if diff := cmp.Diff(tc.want.rsp, rsp, protocmp.Transform()); diff != "" {
106    			t.Errorf("%s\nf.RunFunction(...): -want rsp, +got rsp:\n%s", tc.reason, diff)
107    		}
108
109    		if diff := cmp.Diff(tc.want.err, err, cmpopts.EquateErrors()); diff != "" {
110    			t.Errorf("%s\nf.RunFunction(...): -want err, +got err:\n%s", tc.reason, diff)
111    		}
112    	})
113    }
114}

使用 go test 命令运行单元测试:

1go test -v -cover .
2=== RUN TestRunFunction
3=== RUN TestRunFunction/AddTwoBuckets
4--- PASS: TestRunFunction (0.00s)
5    --- PASS: TestRunFunction/AddTwoBuckets (0.00s)
6PASS
7coverage: 52.6% of statements
8ok github.com/negz/function-xbuckets 0.016s coverage: 52.6% of statements

您可以使用 Crossplane CLI 预览被引用此功能的 Composition 的 Output,不需要使用 Crossplane 控制平面就能完成此操作。

function-xbuckets 下创建名为 example 的目录,并创建 Composite Resource、Composition 和 Function YAML 文件。

展开以下区块,查看示例文件。

您可以使用这些文件,通过运行 crossplane beta render 重现下面的输出结果。

XR.yaml` 文件包含要渲染的 Composition 资源:

 1apiVersion: example.crossplane.io/v1
 2kind: XBuckets
 3metadata:
 4  name: example-buckets
 5spec:
 6  region: us-east-2
 7  names:
 8  - crossplane-functions-example-a
 9  - crossplane-functions-example-b
10  - crossplane-functions-example-c

composition.yaml` 文件包含用于渲染复合资源的 Composition:

 1apiVersion: apiextensions.crossplane.io/v1
 2kind: Composition
 3metadata:
 4  name: create-buckets
 5spec:
 6  compositeTypeRef:
 7    apiVersion: example.crossplane.io/v1
 8    kind: XBuckets
 9  mode: Pipeline
10  pipeline:
11  - step: create-buckets
12    functionRef:
13      name: function-xbuckets

functions.yaml` 文件包含 Composition 在其 Pipelines 步骤中引用的函数:

 1apiVersion: pkg.crossplane.io/v1beta1
 2kind: Function
 3metadata:
 4  name: function-xbuckets
 5  annotations:
 6    render.crossplane.io/runtime: Development
 7spec:
 8  # The CLI ignores this package when using the Development runtime.
 9  # You can set it to any value.
10  package: xpkg.upbound.io/negz/function-xbuckets:v0.1.0

functions.yaml中的函数被引用为开发运行时。 这会告诉crossplane beta render` 您的函数正在本地运行。 它会连接到您本地运行的函数,而不是被引用 Docker 来拉动和运行函数。

1apiVersion: pkg.crossplane.io/v1beta1
2kind: Function
3metadata:
4  name: function-xbuckets
5  annotations:
6    render.crossplane.io/runtime: Development

使用 go run 在本地运行您的函数。

1go run . --insecure --debug
Warning
不安全 不安全标志会告诉函数在不进行加密或身份验证的情况下运行。 只能在测试和开发过程中使用。

在另一个终端中,运行 crossplane beta render

1crossplane beta render xr.yaml composition.yaml functions.yaml

该命令调用你的函数。 在运行函数的终端中,现在应该可以看到 logging 输出:

1go run . --insecure --debug
22023-10-31T16:17:32.158-0700 INFO function-xbuckets/fn.go:29 Running Function        {"tag": ""}
32023-10-31T16:17:32.159-0700 INFO function-xbuckets/fn.go:125 Added desired buckets   {"xr-version": "example.crossplane.io/v1", "xr-kind": "XBuckets", "xr-name": "example-buckets", "region": "us-east-2", "count": 3}

crossplane beta render` 命令会打印函数返回的所需资源。

 1---
 2apiVersion: example.crossplane.io/v1
 3kind: XBuckets
 4metadata:
 5  name: example-buckets
 6---
 7apiVersion: s3.aws.upbound.io/v1beta1
 8kind: Bucket
 9metadata:
10  annotations:
11    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-b
12    crossplane.io/external-name: crossplane-functions-example-b
13  generateName: example-buckets-
14  labels:
15    crossplane.io/composite: example-buckets
16  ownerReferences:
17    # Omitted for brevity
18spec:
19  forProvider:
20    region: us-east-2
21---
22apiVersion: s3.aws.upbound.io/v1beta1
23kind: Bucket
24metadata:
25  annotations:
26    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-c
27    crossplane.io/external-name: crossplane-functions-example-c
28  generateName: example-buckets-
29  labels:
30    crossplane.io/composite: example-buckets
31  ownerReferences:
32    # Omitted for brevity
33spec:
34  forProvider:
35    region: us-east-2
36---
37apiVersion: s3.aws.upbound.io/v1beta1
38kind: Bucket
39metadata:
40  annotations:
41    crossplane.io/composition-resource-name: xbuckets-crossplane-functions-example-a
42    crossplane.io/external-name: crossplane-functions-example-a
43  generateName: example-buckets-
44  labels:
45    crossplane.io/composite: example-buckets
46  ownerReferences:
47    # Omitted for brevity
48spec:
49  forProvider:
50    region: us-east-2
Tip
请阅读composition函数文档,了解有关 测试composition函数 的更多信息。

构建函数并将其推送至 packages 注册表

构建函数分为两个阶段: 首先是构建函数的运行时,这是 Crossplane 用来运行函数的开放容器倡议(OCI)镜像。 然后将运行时嵌入软件包,并将其推送到软件包注册中心。 Crossplane CLI 将 xpkg.upbound.io 作为默认的软件包注册中心。

一个函数默认支持单个平台,如 “linux/amd64”,您可以为每个平台构建运行时和软件包,然后将所有软件包推送到注册表中的单个标签,从而支持多个平台。

将您的函数推送到 registry,就可以在 crossplane 控制平面中使用您的函数。请参阅Composition functions documentation。了解如何在控制平面中使用函数。

使用 docker 为每个平台构建运行时。

1docker build . --quiet --platform=linux/amd64 --tag runtime-amd64
2sha256:fdf40374cc6f0b46191499fbc1dbbb05ddb76aca854f69f2912e580cfe624b4b
1docker build . --quiet --platform=linux/arm64 --tag runtime-arm64
2sha256:cb015ceabf46d2a55ccaeebb11db5659a2fb5e93de36713364efcf6d699069af
Tip
您可以使用任何标签,无需将运行时镜像推送到 registry。 标签只是用来告诉 crossplane xpkg build 嵌入什么运行时。

使用 Crossplane CLI 为每个平台构建一个软件包。 每个软件包都嵌入了一个运行时镜像。

……。 --package-rootflag 指定了包含 crossplane.yamlpackage 目录。 其中包括软件包的元数据。

……。 --嵌入运行时镜像flag 指定了被引用 Docker 构建的运行时镜像标签。

--package-file标志指定将软件包文件写入磁盘的位置。 crossplane 软件包文件的扩展名为 .xpkg

1crossplane xpkg build \
2    --package-root=package \
3    --embed-runtime-image=runtime-amd64 \
4    --package-file=function-amd64.xpkg
1crossplane xpkg build \
2    --package-root=package \
3    --embed-runtime-image=runtime-arm64 \
4    --package-file=function-arm64.xpkg
Tip
crossplane 软件包是特殊的 OCI 镜像。请在【软件包文档】(https://docs.crossplane.io/latest/concepts/packages) 中阅读有关软件包的更多信息。

将两个软件包文件都推送到注册表中。将两个文件都推送到注册表中的一个标签,就能创建一个多平台 软件包,在 linux/arm64linux/amd64 主机上都能运行。

1crossplane xpkg push \
2  --package-files=function-amd64.xpkg,function-arm64.xpkg \
3  negz/function-xbuckets:v0.1.0
Tip

如果您将函数推送到 GitHub 仓库,模板会使用 GitHub Actions 自动设置持续集成 (CI)。CI 工作流将对您的函数进行校验、测试和构建。您可以通过阅读 .github/workflows/ci.yaml,查看模板是如何配置 CI 的。

CI 工作流可以自动将软件包推送到 xpkg.upbound.io。要做到这一点,您必须在 https://marketplace.upbound.io 创建一个版本库。通过创建一个 API 令牌并将其添加到您的版本库,赋予 CI 工作流向市场推送的权限。将您的 API 令牌访问 ID 保存为名为 XPKG_ACCESS_ID 的secret,并将您的 API 令牌保存为名为 XPKG_TOKEN 的secret。